﻿namespace SpikingAnalyticsLib

open System
open System.IO
open System.Collections.Generic
open SpikingNeuronLib

//=================================================================================================

// Extension methods on the SpikingNeuronLib.Pattern type
module PatternExtensions =

    type SpikingNeuronLib.Pattern with

    //---------------------------------------------------------------------------------------------------------
    // Instance Extension methods

    // Compute an integer hash code for the specified Pattern using SHA1
    member this.ComputeHashCode() =
        let GetBytes (pattern:Pattern) =
            pattern.EventList
            |> Seq.map (fun evt ->
                    seq {
                        yield BitConverter.GetBytes(evt.Time)
                        yield BitConverter.GetBytes(evt.NeuronIndex)
                        yield BitConverter.GetBytes((int)evt.Label)
                    }
                )
            |> Seq.concat
            |> Seq.concat
            |> Seq.toArray

        use cryptoProvider = new System.Security.Cryptography.SHA1CryptoServiceProvider()
        let bytes = GetBytes this
        let hash = cryptoProvider.ComputeHash(bytes)

        // return the first four bytes of the hash as an integer
        BitConverter.ToInt32(hash, 0);

    member this.CreateDegradedPattern(proportionToKeep) =
        let initialPattern = this.GetFiringEvents() |> Seq.cast<'a> |> Seq.toList
        let degradedPattern =
            SpikingNeuronLib.Pattern.CreateDegradedPattern(initialPattern, proportionToKeep)
            |> Seq.cast<FiringEvent>
        new SpikingNeuronLib.Pattern(degradedPattern)

    /// Get a string representation of a degraded pattern using the specified characters
    /// to represent the sequence of retained and deleted firing events
    member this.GetDegradedPatternString(retainedCharacter, deletedCharacter) = 
        let firingTimes =
            this.GetEventTuples()
            |> Seq.map (fun (time, nindex) -> time)
            |> Set.ofSeq
                        
        let patternCharacters =
            [1..this.Count]
            |> Seq.map (fun index -> firingTimes.Contains index)
            |> Seq.map (fun kept -> if kept then retainedCharacter else deletedCharacter)
            |> Seq.toArray

        new String(patternCharacters)

    /// Save the firing events in the pattern
    member this.SaveFiringEvents(folder, filename) =
        Pattern.SaveFiringEvents(folder, filename, this.GetEventTuples())

    //---------------------------------------------------------------------------------------------------------
    // Static Extension methods

    /// Create an input pattern using the specified spike sequences for each neuron
    /// where a spike sequence is a sequence of 1s and 0s indicating spiking or quiescence
    static member FromNeuronSpikeSequences(spikeSequences:IDictionary<int, #seq<bool>>) =
        let events =
            spikeSequences
            |> Seq.map (fun kvp ->
                let neuronIndex, spikeSequence = kvp.Key, kvp.Value
                // generate FiringEvents for this neuron
                spikeSequence
                |> Seq.mapi (fun index spike ->
                    if spike then
                        Some(FiringEvent(index, neuronIndex, EventLabel.Unspecified))
                    else
                        None
                )
                |> Seq.choose id
            )
            |> Seq.concat
        new Pattern(events)

    /// Create an input pattern using the specified firing times for each neuron
    /// where each firing time is a one millisecond time slot in a one second frame
    static member FromNeuronFiringTimes(neuronFiringTimes:IDictionary<int, #seq<int>>) =
        let events =
            neuronFiringTimes
            |> Seq.map (fun kvp ->
                let neuronIndex, firingTimes = kvp.Key, kvp.Value
                // generate FiringEvents for this neuron
                firingTimes
                |> Seq.map (fun time -> FiringEvent(time, neuronIndex, EventLabel.Unspecified))
            )
            |> Seq.concat
        new Pattern(events)

    /// Create a Pattern by firing the specified neurons at the specified time
    // was CreateSynchronousPattern
    static member FromSynchronousFiring(time, neurons) =
        let events =
            neurons
            |> Seq.map (fun neuron -> new FiringEvent(time, neuron, EventLabel.Unspecified))
            |> Seq.toArray
        new Pattern(events)

    /// Create a Pattern by matching the specified times and neurons
    // was CreateFiringPattern
    static member FromFiringSequence(times, neurons) =
        let events =
            Seq.zip times neurons
            |> Seq.map (fun (time, neuron) -> new FiringEvent(time, neuron, EventLabel.Unspecified))
            |> Seq.toArray
        new Pattern(events)

    /// Create a Pattern based on a linear pattern of firing (ascending or descending)
    static member FromLinearSequence(minTime, timeStep, maxTime) =
        let events:FiringEvent list = Pattern.CreateLinearSequence(minTime, timeStep, maxTime)
        new Pattern(events)

    /// Combine patterns
    static member Combine(firstPattern:Pattern, secondPattern:Pattern) =
        let events =
            [ firstPattern.EventList; secondPattern.EventList; ]
            |> Seq.concat
        new Pattern(events)

    /// Create a linear firing pattern (ascending or descending)
    // The result can be passed to a function to generate an input pattern
    static member CreateLinearSequence(minTime, timeStep, maxTime) =
        let patternGenerator = PatternGeneratorFactory.CreateLinearPatternGenerator(minTime, timeStep, maxTime)
        patternGenerator 0 0
        |> Seq.toList

    /// Create a degraded version of a list of firing events by randomly selecting
    /// the specified proportion of events and discarding all other events
    static member CreateDegradedPattern(initialPattern:'a list, proportionToKeep) =
        assert (proportionToKeep > 0.0 && proportionToKeep <= 1.0)
        let degradedPattern =
            let keepers =
                let r = new System.Random()
                let maxIndex = initialPattern.Length
                let numberToKeep = int ((float maxIndex) * proportionToKeep)
                Seq.unfold (fun (keepSet:Set<_>) ->
                        if keepSet.Count < maxIndex then
                            let nextIndex = r.Next(maxIndex)
                            let newSet = keepSet.Add nextIndex
                            if newSet.Count <= numberToKeep then
                                Some(nextIndex, newSet)
                            else
                                None
                        else
                            None
                    ) Set.empty
                |> Set.ofSeq
            initialPattern
            |> Seq.mapi (fun index evt -> index, evt)
            |> Seq.filter (fun (index, evt) -> keepers.Contains index)
            |> Seq.map (fun (index, evt) -> evt)
            |> Seq.toList

        degradedPattern

    /// Save the specified sequence of firing event tuples to a file
    static member SaveFiringEvents(folder, filename, firingEvents:seq<int * int * float>) =
        let filePath = Path.Combine(folder, filename)

        let lines =
            firingEvents
            |> Seq.map (fun (time, nindex, probability) -> sprintf "%d\t%d\t%f" time nindex probability)

        File.WriteAllLines(filePath, lines)

    /// Save the specified sequence of firing event tuples to a file
    static member SaveFiringEvents(folder, filename, firingEvents:seq<int * int>) =
        let filePath = Path.Combine(folder, filename)

        let lines =
            firingEvents
            |> Seq.map (fun (time, nindex) -> sprintf "%d\t%d" time nindex)

        File.WriteAllLines(filePath, lines)

    /// Save the specified sequence of firing events to a file
    static member SaveFiringEvents(folder, filename, firingEvents:FiringEvent list) =
        let eventTuples =
            firingEvents
            |> Seq.map (fun evt -> evt.Time, evt.NeuronIndex)
        Pattern.SaveFiringEvents(folder, filename, eventTuples)

    // Extract a Stimulus from the specified line optionally limiting the maximum length
    static member ParseFromString(line, maxSize:int option, setUniqueID) =
        let pattern =
            let fullPattern =
                let eventList =
                    let events =
                        let tupleList = IOUtilities.ParseStringToList<int * int * EventLabel>(line)
                        tupleList
                        |> Seq.map (fun (time, nindex, label) -> new FiringEvent(time, nindex, label))
                    new List<FiringEvent>(events)
                new Pattern(eventList)
            if setUniqueID then
                let hash = fullPattern.ComputeHashCode() 
                fullPattern.SetUniqueID(hash)
            if maxSize.IsSome then
                // the UniqueID is inherited from the parent Pattern allowing parent and child to be linked
                // allows the full and truncated patterns to be linked
                new Pattern(fullPattern, maxSize.Value)
            else
                fullPattern
        pattern

    /// Create an interleave between two or more patterns
    static member CreatePatternInterleave(blockSize, blockSequenceLength, patterns:Pattern list) =
        // Create an interleave between two or more patterns
        // The blockSize is the timerange that defines a block of (usually repeated) events e.g. blockSize = 40
        // The blockSequenceLength is the number of blocks in a repeated sequence of blocks taken from the same pattern
        // e.g. blockSequenceLength = 3 with two patterns selects events as follows: 0 0 0 1 1 1 0 0 0 1 1 1 ...
        let InterleavePatterns blockSize blockSequenceLength (patterns:Pattern list) =
            let interleavePattern =
                let numPatterns = patterns.Length
                let numBlocks = 1000 / blockSize
                Seq.initInfinite (fun index ->
                    seq {
                        for i in 0..numPatterns-1 do
                            for j in 1..blockSequenceLength do
                                yield i
                    }
                )
                |> Seq.concat
                |> Seq.take numBlocks
                |> Seq.toList
            let blockEventMap =
                patterns
                |> Seq.map (fun (pattern:Pattern) ->
                        pattern.EventList
                        |> Seq.fold (fun (state:Map<int, FiringEvent list>) event ->
                                let blockIndex = event.Time / blockSize
                                let blockEvents =
                                    if state.ContainsKey blockIndex then
                                        state.[blockIndex]
                                    else
                                        []
                                Map.add blockIndex (event :: blockEvents) state
                            ) Map.empty
                    )
                |> Seq.toArray
            let interleaved =
                interleavePattern
                |> Seq.mapi (fun blockIndex selectedIndex ->
                        blockEventMap.[selectedIndex].[blockIndex]
                        |> List.toSeq
                    )
                |> Seq.concat
                |> Seq.toArray

            interleaved

        assert (blockSize > 0 && blockSize <= 1000)
        let eventArray = InterleavePatterns blockSize blockSequenceLength patterns
        new List<FiringEvent>(eventArray)
